Skip to content

Add a live word / character count indicator to the chat composer#61

Open
DragonnZhang wants to merge 2 commits into
mainfrom
loop/composer-word-count
Open

Add a live word / character count indicator to the chat composer#61
DragonnZhang wants to merge 2 commits into
mainfrom
loop/composer-word-count

Conversation

@DragonnZhang

Copy link
Copy Markdown
Collaborator

Closes #60

What & why

OpenWork's chat composer (FreeFormInput) gave no feedback about how much
you've written. The toolbar already surfaces a context-usage indicator, a model
picker, a thinking-level picker and an attach control — but there was no length
feedback for the draft itself
, which comparable desktop composers/editors
(Codex desktop, VS Code / Google Docs status bars, the Claude/ChatGPT message
boxes) all provide.

This adds a small, unobtrusive draft count indicator to the composer toolbar:

  • shows the word count inline (e.g. Words: 12), only while the composer has
    non-whitespace text — so the empty composer stays clean;
  • a hover tooltip breaks down the full stats: Words, Characters, and
    Lines;
  • counts update live per keystroke, derived from the existing controlled input
    text (no new state, no persistence);
  • hidden in compactMode (mirroring the context / thinking / model controls) and
    hidden entirely when the draft is empty.

Frontend-only. No qwen-code backend change — the composer already stores the
draft as a plain-text string, so the indicator is pure derived state.

Changes

  • lib/composer-count.ts — pure computeComposerCounts /
    shouldShowComposerCount helpers (Unicode-aware character count via code
    points, whitespace-collapsing word count, newline-based line count), DOM-free
    and unit-tested.
  • components/app-shell/input/ComposerCountIndicator.tsx — the indicator:
    renders null for an empty draft, shows the word count inline with the full
    breakdown in a tooltip, and exposes data-word-count / data-char-count /
    data-line-count for e2e.
  • components/app-shell/input/FreeFormInput.tsx — slots the indicator into
    the toolbar bottom row (between the left badge group and the flex spacer,
    !compactMode).
  • i18n — three new keys (chat.composerWords, chat.composerCharacters,
    chat.composerLines) added to all 7 locales, using a plural-neutral label
    form
    ("Words: {{count}}") so no per-locale plural categories are needed.
  • e2e/assertions/composer-count.assert.ts — new CDP assertion (below).

Verification (DoD)

  • bun run typecheck:allzero new errors. packages/{core,shared,server-core,server,session-tools-core,ui} all pass clean; apps/electron shows only its 11 pre-existing baseline errors (auto-update.ts owner/repo ×4, a settings-default-thinking test tuple ×5, two test files importing vitest) — none in the files this PR touches.
  • Unit testscomposer-count.test.ts 10/10 pass (empty / whitespace-only, single vs. multiple words, collapsed internal whitespace, multi-line, emoji/Unicode character counting). Touched-area suites (renderer/lib) 141/141 pass; the one unrelated SAFE_MODE_CONFIG failure pre-dates this change.
  • bun run lint:i18n:parity — OK (6 locales, 1545 keys each — +3, all present in every locale).
  • ESLint — clean on the new/changed files.
  • Renderer build (bun run electron:build:renderer) — ✅ builds cleanly.
  • CDP assertion transpiles (bun build e2e/assertions/composer-count.assert.ts) — ✅.

CDP e2e assertion (composer-count.assert.ts)

Drives the real built app over CDP entirely in the draft (no-session) state — no
seeded conversation and no backend:

  1. While the composer is empty, the count indicator ([data-testid="composer-count"]) is absent.
  2. Real typing (CDP Input.insertText) of hello world makes it appear with data-word-count="2" and data-char-count="11".
  3. Appending again updates the counts live to data-word-count="3" / data-char-count="17" — proving per-keystroke reactivity.
  4. Selecting-all and deleting the draft makes the indicator disappear again.

⚠️ Local CDP run was blocked by this sandbox's egress policy, not by the
feature — the e2e build fails before launch because the Electron binary
download (GitHub release host) returns 403 and the libsignal WhatsApp-worker
dependency can't be fetched, the same limitation noted on the other open
loop-bot PRs. The assertion transpiles and is included so CI / a reviewer can
run bun run e2e in an environment with normal network access. Everything that
doesn't require launching Electron (typecheck, unit tests, i18n parity,
renderer build) passes.

Part of the autonomous desktop-feature loop (loop-bot).


Generated by Claude Code

Adds a small draft count indicator to the composer toolbar that shows the
current word count inline (hidden when the draft is empty or in compact mode)
with a hover tooltip breaking down words, characters, and lines.

- composer-count.ts: pure computeComposerCounts/shouldShowComposerCount helpers
  (Unicode-aware character count, whitespace-collapsing words, newline lines)
  with unit tests.
- ComposerCountIndicator.tsx: derived-state indicator over the composer's
  existing plain-text input; exposes data-* counts for e2e.
- Wires the indicator into the FreeFormInput toolbar bottom row.
- Adds chat.composerWords/Characters/Lines i18n keys (plural-neutral label
  form) to all 7 locales.
- e2e/assertions/composer-count.assert.ts: CDP assertion driving real typing.

Closes #60
@DragonnZhang DragonnZhang added enhancement New feature or request loop-bot labels Jul 6, 2026
Resolve merge conflict in docs/loop/feature-ledger.md (append-only ledger)
by keeping the branch's reconciled superset, which already includes the
reduce-motion (#51) row plus all newer loop entries. Locale files auto-merged
cleanly; i18n parity verified (7 locales, 1547 keys each, no dups).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request loop-bot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Live word / character count indicator in the chat composer

2 participants